package cn.wolfcode.service.impl;

import cn.wolfcode.common.exception.BusinessException;
import cn.wolfcode.common.utils.AssertUtil;
import cn.wolfcode.common.utils.IdGenerateUtil;
import cn.wolfcode.common.web.Result;
import cn.wolfcode.domain.*;
import cn.wolfcode.feign.IntergralFeignApi;
import cn.wolfcode.feign.PayFeignApi;
import cn.wolfcode.mapper.OrderInfoMapper;
import cn.wolfcode.mapper.PayLogMapper;
import cn.wolfcode.mapper.RefundLogMapper;
import cn.wolfcode.mapper.SeckillProductMapper;
import cn.wolfcode.mq.DefaultMessageSendCallback;
import cn.wolfcode.mq.MQConstant;
import cn.wolfcode.mq.OrderMessage;
import cn.wolfcode.redis.SeckillRedisKey;
import cn.wolfcode.service.IOrderInfoService;
import cn.wolfcode.service.ISeckillProductService;
import cn.wolfcode.util.DateUtil;
import cn.wolfcode.web.msg.SeckillCodeMsg;
import com.alibaba.fastjson.JSON;
import io.seata.core.context.RootContext;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestBody;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;

/**
 * Created by wolfcode
 */
@CacheConfig(cacheNames = "orders")
@Slf4j
@Service
public class OrderInfoSeviceImpl implements IOrderInfoService {
    private final ISeckillProductService seckillProductService;
    private final SeckillProductMapper seckillProductMapper;
    private final OrderInfoMapper orderInfoMapper;
    private final StringRedisTemplate redisTemplate;
    private final PayLogMapper payLogMapper;
    private final RefundLogMapper refundLogMapper;
    private final RocketMQTemplate rocketMQTemplate;
    private final PayFeignApi payFeignApi;
    private final IntergralFeignApi intergralFeignApi;

    public OrderInfoSeviceImpl(ISeckillProductService seckillProductService, SeckillProductMapper seckillProductMapper, OrderInfoMapper orderInfoMapper, StringRedisTemplate redisTemplate, PayLogMapper payLogMapper, RefundLogMapper refundLogMapper, RocketMQTemplate rocketMQTemplate, PayFeignApi payFeignApi, IntergralFeignApi intergralFeignApi) {
        this.seckillProductService = seckillProductService;
        this.seckillProductMapper = seckillProductMapper;
        this.orderInfoMapper = orderInfoMapper;
        this.redisTemplate = redisTemplate;
        this.payLogMapper = payLogMapper;
        this.refundLogMapper = refundLogMapper;
        this.rocketMQTemplate = rocketMQTemplate;
        this.payFeignApi = payFeignApi;
        this.intergralFeignApi = intergralFeignApi;
    }

    @Override
    public OrderInfo selectByUserIdAndSeckillId(Long userId, Long seckillId, Integer time) {
        return orderInfoMapper.selectByUserIdAndSeckillId(userId, seckillId, time);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public OrderInfo doSeckill(Long phone, SeckillProductVo sp) {
        //1.扣减库存
        seckillProductService.decrStockCount(sp.getId(), sp.getTime());
        //2.创建订单
        OrderInfo orderInfo = this.buildOrderInfo(phone, sp);
        //3.保存订单
        orderInfoMapper.insert(orderInfo);
        //模拟异常，测试进行库存数量回滚，删除用户已下单表示，取消本地库存售完标识
        //int i = 1/0;
        //4.返回订单的id
        return orderInfo;
    }

    //缓存订单数据，新增操作，无论是否存在缓存，都添加缓存
    @CachePut(key = "'detail:' + #result.orderNo")
    @Transactional(rollbackFor = Exception.class)
    @Override
    public OrderInfo doSeckill(Long phone, Integer time, Long seckillId) {
        //基于秒杀 id + 场次查询秒杀商品对象
        SeckillProductVo sp = seckillProductService.selectByIdAndTime(seckillId, time);
        return this.doSeckill(phone, sp);
    }

    @Cacheable(key = "'detail:' + #orderNo")
    @Override
    public OrderInfo findByOrderNo(String orderNo) {
        return orderInfoMapper.findByOrderNo(orderNo);
    }

    @Override
    public void orderCreateFailedRollback(OrderMessage orderMessage) {
        //1.回补Redis中的预减库存,直接+1，但是当库存预减为负数时，此时+1相当于也是负数，则没意义
        //解决方案：通过查询mysql中库存数量,进行覆盖操作
        //当查询mysql中库存时，但是另一边也在扣库存，此时虽然数据不一致不影响，因为Redis中的库存数量只是库存预减，减少请求而已。并且后续扣库存还有乐观锁的控制。
        int stockCount = seckillProductService.selectStockCountById(orderMessage.getSeckillId());
        String hashKey = SeckillRedisKey.SECKILL_STOCK_COUNT_HASH.join(orderMessage.getTime());
        redisTemplate.opsForHash().put(hashKey, orderMessage.getSeckillId() + "", stockCount + "");

        //2.删除Redis中用户是否重复下单的标识
        String userOrderFlagKey = SeckillRedisKey.SECKILL_ORDER_HASH.join(orderMessage.getSeckillId() + "");
        redisTemplate.opsForHash().delete(userOrderFlagKey, orderMessage.getUserPhone() + "");

        //3.删除本地标识库存售完标记
        //单体：OrderInfoController.removeStockOverFlagKey(orderMessage.getSeckillId());
        //问题：如果在集群环境下，有三个Map，无法保证删除的是哪个服务下的Map
        //解决方案：通过RocketMQ广播模式通知每个服务都去删除
        rocketMQTemplate.asyncSend(
                MQConstant.CANCEL_SECKILL_OVER_SIGE_TOPIC,
                orderMessage.getSeckillId() + "",
                new DefaultMessageSendCallback("删除本地标识库存售完标记")
        );
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void checkOrderPayTimeout(OrderMessage orderMessage) {
        //检查订单状态是否为 未支付状态，如果未支付状态，则状态改为：取消订单，修改成功进行数据回滚
        //1.回滚MySQL
        //1.1 修改订单状态：取消订单
        int row = orderInfoMapper.updateCancelStatus(orderMessage.getOrderNo(), OrderInfo.STATUS_TIMEOUT);
        if (row > 0) {
            //取消订单，进行回滚操作
            //回补MySQL,回补Redis，删除Redis重复下单,删除本地标识
            this.seckillAllRollback(orderMessage);
        }
    }

    /**
     * 秒杀商品的所有回滚操作，如：MySQL库存回补，Redis库存回补，删除Redis用户购买标识，删除本地商品售完标识
     * 有事务的方法，调用了该方法没有事务，而调用者和被调用者事务都生效，因为调用者有事务，由于事务有传播机制，因此被调用者也具有事务
     *
     * @param orderMessage
     */
    private void seckillAllRollback(OrderMessage orderMessage) {
        //1.2 库存数量回补
        seckillProductMapper.incrStock(orderMessage.getSeckillId());
        //2.回滚 Redis(覆盖Redis库存数量，取消用户重复下单标识)
        //3.取消本地标识
        this.orderCreateFailedRollback(orderMessage);
    }

    @Override
    public String onlinePay(String orderNo, Long phone) {
        //1.根据订单号获取订单信息
        OrderInfo orderInfo = orderInfoMapper.findByOrderNo(orderNo);
        //2.判断订单状态，是否为未支付状态，只有未支付状态才允许发起支付请求
        AssertUtil.isTrue(OrderInfo.STATUS_ARREARAGE.equals(orderInfo.getStatus()), "订单状态异常，无法发起支付");

        //发起支付前，判断当前用户是否是创建这个订单的用户
        AssertUtil.isTrue(orderInfo.getUserId().equals(phone), "非法操作");

        //3.封装支付参数
        PayVo payVo = new PayVo();
        payVo.setOutTradeNo(orderInfo.getOrderNo());//订单编号
        payVo.setSubject(orderInfo.getProductName()); //订单名称，必填
        payVo.setTotalAmount(orderInfo.getSeckillPrice().toString()); //付款金额，必填
        payVo.setBody("限时抢购，支付宝支付:" + orderInfo.getProductName());//商品描述，可空
        //设置超时未支付时间,格式:2016-12-31 10:05:01
        String timeoutDateTime = LocalDateTime.now().plusMinutes(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        payVo.setTimeExpire(timeoutDateTime);

        //4.远程调用支付服务进行支付操作
        Result<String> result = payFeignApi.prepay(payVo);
        return result.checkAndGet();
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void changeOrderStateAndPayLog(@RequestBody PayResult payResult) {
        //1.根据订单id，查询订单信息，验证金额有误
        OrderInfo orderNo = this.findByOrderNo(payResult.getOutTradeNo());
        AssertUtil.notNull(orderNo, "订单查询失败");

        //2.验证支付金额是否正确
        BigDecimal totalAmount = new BigDecimal(payResult.getTotalAmount());
        int result = totalAmount.compareTo(orderNo.getSeckillPrice());
        AssertUtil.isTrue(result <= 0, "支付金额有误");

        //3.修改订单状态： order_no(订单号),status(支付状态),pay_date(支付时间),pay_type(支付类型)
        //TODO 修改订单状态之前，要判断当前状态是  未支付状态
        int statusRow = orderInfoMapper.changePayStatus(
                payResult.getOutTradeNo(),
                OrderInfo.STATUS_ACCOUNT_PAID,
                OrderInfo.PAY_TYPE_ONLINE);
        AssertUtil.isTrue(statusRow > 0, "订单状态修改失败");

        //4.记录支付记录：out_trade_no(订单号),trade_no(支付宝交易流水号),notify_time(支付时间),total_amount(支付总金额),pay_type(支付类型)
        PayLog payLog = this.buildPayLog(payResult);
        int payLogRow = payLogMapper.insert(payLog);
        AssertUtil.isTrue(payLogRow > 0, "支付日志记录失败");
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void refundByAlipay(String orderNo) {
        //1.根据订单编号，查询商品是否已经支付，已支付则可进行退款操作
        OrderInfo orderInfo = orderInfoMapper.findByOrderNo(orderNo);
        AssertUtil.notNull(orderInfo, "订单查询失败!");

        //2.只有支付状态才能退
        AssertUtil.isTrue(OrderInfo.STATUS_ACCOUNT_PAID.equals(orderInfo.getStatus()), "订单状态错误");

        //3.支付类型
        String refundReason = "用户申请退款：" + orderInfo.getProductName();

        //4.根据订单编号，查询支付流水时间，判断是否在7天内，否则不支持退款
        PayLog payLog = payLogMapper.findByOrderNo(orderNo);
        String notifyTime = payLog.getNotifyTime();
        Boolean isInDate = DateUtil.betweenDate(notifyTime, 7);
        AssertUtil.isTrue(isInDate, SeckillCodeMsg.REFUND_OUTDATE_ERROR);

        //5.退款
        Result<Boolean> refund = null;
        //支付类型：支付宝支付，则退款：支付宝退款
        if (OrderInfo.PAY_TYPE_ONLINE == orderInfo.getPayType()) {
            //远程调用发起支付宝退款操作
            RefundVo refundVo = new RefundVo(
                    orderInfo.getOrderNo(),
                    orderInfo.getSeckillPrice().toString(),
                    refundReason);
            refund = payFeignApi.refund(refundVo);
        } else {
            //支付类型：积分支付，则退款：积分退款
            OperateIntergralVo intergralVo = this.buildOperateIntergralVo(orderInfo, "订单积分退款");
            log.info("[积分退款] 准备发起积分退款 {}", JSON.toJSONString(intergralVo));
//            refund = intergralFeignApi.refund(intergralVo);
            //如果远程积分退款成功，而本地日志记录失败，导致异常，需要进行本地事务和远程事务回滚
            //解决方案：RocketMQ的事务消息，本地事务执行成功，而远程事务就必定成功,否则就回滚
            //1.向RocketMQ发送一条事务消息
            Message<OperateIntergralVo> message = MessageBuilder.withPayload(intergralVo).setHeader("outTradeNo", orderNo).build();
            TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(MQConstant.INTERGRAL_REFUND_TX_PRODUCT_GROUP,
                    MQConstant.INTERGRAL_REFUND_TX_TOP, message, orderNo);
            //3.本地事务返回的消息状态
            LocalTransactionState localTransactionState = sendResult.getLocalTransactionState();
            //如果返回的本地事务消息状态为：COMMIT 或 UNKNOW 则代表本地事务执行成功
            if (localTransactionState.equals(LocalTransactionState.COMMIT_MESSAGE)
                    ||
                    localTransactionState.equals(LocalTransactionState.UNKNOW)
            ) {
                //如果本地事务执行成功，则表示退款成功
                log.info("[积分退款] 积分退款本地事务执行完成，等待远程服务执行状态：{}", localTransactionState);
                return;
            }
            //rollback，则本地事务执行失败，则表示退款失败,则抛异常
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }

        //5.支付服务退款成功后进行以下操作
        if (refund == null || refund.hasError() || !refund.getData()) {
            throw new BusinessException(SeckillCodeMsg.REFUND_ERROR);
        }

        //退款成功=》修订订单状态，库存回补，删除用户下单标识，删除本地标识，记录退款日志
        this.refundRollback(orderInfo);
    }

    /**
     * 退款回调函数:修订订单状态，库存回补，删除用户下单标识，删除本地标识，记录退款日志
     *
     * @param orderInfo
     */
    private void refundRollback(OrderInfo orderInfo) {
        //6.只有订单为已支付状态，修改订单状态为已退款
        int row = orderInfoMapper.changeRefundStatus(orderInfo.getOrderNo(), OrderInfo.STATUS_REFUND);
        AssertUtil.isTrue(row > 0, "订单状态修改失败!");

        //7. 回补操作
        // 7.1.mysql库存回补
        // 7.2 redis库存回补，删除redis中用户下单标记
        // 7.3 广播模式，删除本地标识库存售完
        OrderMessage orderMessage = new OrderMessage();
        orderMessage.setTime(orderInfo.getSeckillTime());
        orderMessage.setSeckillId(orderInfo.getSeckillId());
        orderMessage.setUserPhone(orderInfo.getUserId());
        orderMessage.setOrderNo(orderInfo.getOrderNo());
        this.seckillAllRollback(orderMessage);

        //8.记录退款信息
        RefundLog refundLog = new RefundLog(
                orderInfo.getOrderNo(),
                orderInfo.getSeckillPrice().toString(),
                "用户申请退款：" + orderInfo.getProductName(),
                orderInfo.getPayType(),
                new Date());
        int saveRefundRow = refundLogMapper.insert(refundLog);
        AssertUtil.isTrue(saveRefundRow > 0, "退款日志记录失败!");
    }

    @GlobalTransactional
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void integralPay(String orderNo, Long phone) {
        //1.判断当前状态是否未支付状态
        OrderInfo orderInfo = orderInfoMapper.findByOrderNo(orderNo);
        AssertUtil.notNull(orderInfo, "订单查询失败");
        AssertUtil.isTrue(OrderInfo.STATUS_ARREARAGE.equals(orderInfo.getStatus()), "订单状态错误");

        //2.支付前，判断当前用户是否是创建该订单的用户
        AssertUtil.isTrue(orderInfo.getUserId().equals(phone), "非法操作");

        //3.封装积分支付参数
        OperateIntergralVo intergralVo = this.buildOperateIntergralVo(orderInfo, "订单积分支付");

        //4.发起积分扣除操作
        Result<IntergralPayResult> result = intergralFeignApi.pay(intergralVo);
        IntergralPayResult intergralPayResult = result.checkAndGet();

        //5.支付完成后，判断支付金额是否正确
        AssertUtil.isTrue(intergralVo.getValue().equals(intergralPayResult.getAmount()), "支付积分金额有误");

        //6.修改订单状态
        int statusRow = orderInfoMapper.changePayStatus(orderNo, OrderInfo.STATUS_ACCOUNT_PAID, OrderInfo.PAY_TYPE_INTERGRAL);
        AssertUtil.isTrue(statusRow > 0, "订单状态修改失败");

        //7.积分支付日志记录
        PayLog payLog = this.buildPayLog(intergralPayResult);
        int logSaveRow = payLogMapper.insert(payLog);
        AssertUtil.isTrue(logSaveRow > 0, "订单支付之积分支付日志记录失败");

        //模拟异常，分布式事务的产生
        //int i = 1/0;
    }

    @Override
    public void integralRefundRollback(String orderNo) {
        OrderInfo orderInfo = orderInfoMapper.findByOrderNo(orderNo);
        this.refundRollback(orderInfo);
    }

    /**
     * 封装积分支付参数
     *
     * @param orderInfo
     * @return
     */
    private OperateIntergralVo buildOperateIntergralVo(OrderInfo orderInfo, String infoPrefix) {
        OperateIntergralVo intergralVo = new OperateIntergralVo();
        //订单编号
        intergralVo.setOutTradeNo(orderInfo.getOrderNo());
        //订单扣除积分
        intergralVo.setValue(orderInfo.getIntergral());
        //说明信息
        intergralVo.setInfo(infoPrefix + ":" + orderInfo.getProductName());
        //用户id
        intergralVo.setUserId(orderInfo.getUserId());
        return intergralVo;
    }

    /**
     * 封装订单支付之积分支付日志记录对象
     *
     * @param payResult
     * @return
     */
    private PayLog buildPayLog(IntergralPayResult payResult) {
        PayLog payLog = new PayLog();
        payLog.setOutTradeNo(payResult.getOutTradeNo());
        payLog.setTradeNo(payResult.getTradeNo());
        payLog.setTotalAmount(payResult.getAmount() + "");
        payLog.setPayType(PayLog.PAY_TYPE_INTERGRAL);
        String datetime = String.valueOf(System.currentTimeMillis());
        payLog.setNotifyTime(datetime);
        return payLog;
    }

    /**
     * 订单支付之支付宝支付日志记录对象
     *
     * @param payResult
     * @return
     */
    private PayLog buildPayLog(PayResult payResult) {
        PayLog payLog = new PayLog();
        payLog.setOutTradeNo(payResult.getOutTradeNo());
        payLog.setTradeNo(payResult.getTradeNo());
        payLog.setTotalAmount(payResult.getTotalAmount());
        payLog.setPayType(PayLog.PAY_TYPE_ONLINE);
        String datetime = String.valueOf(System.currentTimeMillis());
        payLog.setNotifyTime(datetime);
        return payLog;
    }

    /**
     * 封装订单信息对象
     *
     * @param phone
     * @param vo
     * @return
     */
    private OrderInfo buildOrderInfo(Long phone, SeckillProductVo vo) {
        Date now = new Date();
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setCreateDate(now);
        orderInfo.setDeliveryAddrId(1L);
        orderInfo.setIntergral(vo.getIntergral());
        //ID生成器 => 雪花算法，生成分布式唯一ID
        orderInfo.setOrderNo(IdGenerateUtil.get().nextId() + "");
        orderInfo.setPayType(OrderInfo.PAY_TYPE_ONLINE);
        orderInfo.setProductCount(1);
        orderInfo.setProductId(vo.getProductId());
        orderInfo.setProductImg(vo.getProductImg());
        orderInfo.setProductName(vo.getProductName());
        orderInfo.setProductPrice(vo.getProductPrice());
        orderInfo.setSeckillDate(now);
        orderInfo.setSeckillId(vo.getId());
        orderInfo.setSeckillPrice(vo.getSeckillPrice());
        orderInfo.setSeckillTime(vo.getTime());
        orderInfo.setStatus(OrderInfo.STATUS_ARREARAGE);
        orderInfo.setUserId(phone);
        return orderInfo;
    }
}
